﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Data;
using LumenWorks.Framework.IO.Csv;
using System.IO;
using System.Drawing;
using Calcite.Classes;
using Calcite.Classes.Misc;
using Calcite.Properties;

namespace Calcite.Classes
{
    /// <summary>
    /// The display grid control for the data in the Worksheet
    /// </summary>
    public partial class WorksheetGridView : DataGridView
    {
        /// <summary>
        /// DataTable bound to the WorksheetGridView instance
        /// </summary>
        public DataTable data = new DataTable();
        /// <summary>
        /// BindingSource actually bound to the WorksheetGridView instance
        /// </summary>
        public BindingSource dataHandle = new BindingSource();
        
        public WorksheetGridView()
        {
            InitializeComponent();

            this.AutoGenerateColumns = true;
            // Resize column headers automatically
            this.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;

        }

        /// <summary>
        /// Necessary to bind data outside of constructor, should be done when DataGridView is loaded, generally done by parent form
        /// </summary>
        internal void BindData()
        {
            int i;
            int numBlankRows = ApplicationSettings.Default.DefaultNumWorksheetRows;
            int numBlankCols = ApplicationSettings.Default.DefaultNumWorksheetColumns;
            // Add columns - i used for naming
            for (i = 0; i < numBlankCols; i++)
            {
                this.AddColumn();
            }
            // Add rows
            for (i = 0; i < numBlankRows; i++)
            {
                this.AddRow();
            }
            // Link data to the handle for binding
            this.dataHandle.DataSource = this.data;
            // Bind the data
            this.RefreshView();
        }


        /// <summary>
        /// Set the string which prefixes the number in the column header
        /// </summary>
        [BrowsableAttribute(true),
        CategoryAttribute("Appearance"),
        DescriptionAttribute("The sting prefix before the number")]
        public string ColPrefix {get; set;}

        private bool _isPrincipleGrid;
        /// <summary>
        /// Should this be the default focus when visible?
        /// Can be assigned at design time
        /// </summary>
        [BrowsableAttribute(true),
        CategoryAttribute("Focus"),
        DescriptionAttribute("Should this be the default focus when visible?")]
        public bool IsPrincipleGrid
        {
            get
            {
                return _isPrincipleGrid;
            }
            set
            {
                _isPrincipleGrid = value;
            }
        }

        /// <summary>
        /// Pastes clipboard content to cells if valid - can overwrite cells (like Excel) or create new 
        /// rows or columns to acommodate
        /// </summary>
        /// <param name="mode">One of "overwrite", "insertcols", "insertrows"</param>
        public void PasteToCells(string mode)
        {
            int i;
            if (Clipboard.ContainsText())
            {
                string clipBoardContent = Clipboard.GetText();
                using (CsvReader pastedCsvReader = new CsvReader(
                    new StringReader(clipBoardContent), false, '\t'))
                {
                    int numPastedCols = pastedCsvReader.FieldCount;
                    CellCoords insertPoint = GetSelectedInsertPoint();
                    // Make space for columns if needed
                    if (mode == "insertcols")
                    {
                        for (i = 0; i < numPastedCols; i++)
                        {
                            this.InsertColumn(insertPoint.Column);
                        }
                    }
                    else 
                    {
                        // If pasted data is wider than current bounds of data, then expand accordingly
                        while ((numPastedCols + insertPoint.Column) > this.data.Columns.Count)
                        {
                            this.AddColumn();
                        }
                    }
                    while  (pastedCsvReader.ReadNextRecord()) 
                    {
                        // Make space for rows (row by row) if needed
                        if (mode == "insertrows")
                        {
                            this.data.Rows.InsertAt(this.data.NewRow(), insertPoint.Row);
                        }
                        else
                        {
                            // May add an extra row, not a big deal I hope
                            if ((insertPoint.Row + 1) > this.data.Rows.Count)
                            {
                                this.AddRow();
                            }
                        }
                        // Populate the cells with valid pasted text
                        for (i = 0; i < numPastedCols; i++)
                        {
                            this.data.Rows[insertPoint.Row][insertPoint.Column + i] = this.ConvertCellContent(pastedCsvReader[i]);
                        }
                        insertPoint.Row = insertPoint.Row + 1;
                    }
                    

                    
                }

            }
            else
            {
                // TODO: Do nothing?
                Console.WriteLine("No text on clipboard");
            }
        }

        /// <summary>
        /// Calculated the insert point of a selected cell or set of selected cells for the purpose of i.e. pasting
        /// </summary>
        /// <param name="rowIndex">The row index</param>
        /// <param name="columnIndex">The column index</param>
        internal CellCoords GetSelectedInsertPoint()
        {
            CellCoords cellCoords = new CellCoords(0,0);
            // If there are no selected cells, return cell at [0,0]
            if (this.SelectedCells.Count == 0)
            {
                return cellCoords;
            }
            else
            {
                // Set row and column index to first cell then iterate over all others, 
                // if come accross an index which is smaller then change to that. May 
                // act weird with 'spotty' selections but then what to expect?
                cellCoords.Column = this.SelectedCells[0].ColumnIndex;
                cellCoords.Row = this.SelectedCells[0].RowIndex;
                foreach (DataGridViewCell tmpCell in this.SelectedCells)
                {
                    if (tmpCell.ColumnIndex < cellCoords.Column)
                    {
                        cellCoords.Column = tmpCell.ColumnIndex;
                    }
                    if (tmpCell.RowIndex < cellCoords.Row)
                    {
                        cellCoords.Row = tmpCell.RowIndex;
                    }
                }
                return cellCoords;
            }
        }
        
        /// <summary>
        /// Returns a number or a blank cell if string is invalid
        /// </summary>
        /// <param name="testCase">String to test</param>
        /// <returns></returns>
        public object ConvertCellContent(string testCase)
        {
            // TODO: Make generic, for now assume that any double entry is OK
            //var numType = Type.GetType(Config.Application.dataType);
            double parsedNum;
            if (Double.TryParse(testCase, 
                System.Globalization.NumberStyles.Any, 
                System.Globalization.NumberFormatInfo.InvariantInfo, 
                out parsedNum))
            // OK if cell validates as number
            {
                return parsedNum;
            }
            else if (testCase == DBNull.Value.ToString())
            // OK if cell is blank 
            {
                return DBNull.Value;
            }
            else
            {
                // Redundant, but may be needed in future
                return DBNull.Value;
            }
        }

        /// <summary>
        /// Returns true if the string passed is a valid cell
        /// </summary>
        /// <param name="testCase">Test string</param>
        /// <returns></returns>
        public bool IsValidCellContent(string testCase)
        // Returns a boolean depending on whether a string is valid cell content
        {
            // TODO: Make generic, for now assume that any double entry is OK
            //var numType = Type.GetType(Config.Application.dataType);

            // Throwaway double for TryParse method
            double parsedNum;
            if (Double.TryParse(testCase,
                System.Globalization.NumberStyles.Any,
                System.Globalization.NumberFormatInfo.InvariantInfo,
                out parsedNum))
            // OK if cell validates as number
            {
                return true;
            }
            else if (testCase == DBNull.Value.ToString())
            // OK if cell is blank 
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Append a column to the DataTable
        /// </summary>
        internal void AddColumn()
        {
            this.InsertColumn(-1);
        }

        /// <summary>
        /// Add a row onto the end of the DataTable
        /// </summary>
        internal void AddRow()
        {
            DataRow tmpRow = this.data.NewRow();
            this.data.Rows.Add(tmpRow);
        }

        /// <summary>
        /// AddRow overload. Adds a row and populates with contents of IEnumberable of strings
        /// resizes table appropriately
        /// </summary>
        /// <param name="contents"></param>
        internal void AddRow(IEnumerable<string> contents)
        {
            int i = 1, lastRowIndex;
            this.AddRow();
            lastRowIndex = this.data.Rows.Count -1;
            foreach (string cellContent in contents)
            {
                if (i > this.data.Columns.Count)
                {
                    this.AddColumn();
                }
                this.data.Rows[lastRowIndex][i] = cellContent;
            }
        }

        /// <summary>
        /// Insert a row into DataTable at the specified index
        /// </summary>
        /// <param name="index">Row index</param>
        internal void InsertRow(int index)
        {
            DataRow tmpRow = this.data.NewRow();
            this.data.Rows.InsertAt(tmpRow, index);
        }

        /// <summary>
        /// Inserts a row into DataTable at the relevant select point
        /// </summary>
        internal void InsertRowAtSelection()
        {
            CellCoords insertPoint = this.GetSelectedInsertPoint();
            this.InsertRow(insertPoint.Row);
        }

        /// <summary>
        /// Inserts a column into DataTable at relevant selection point
        /// </summary>
        internal void InsertColumnAtSelection()
        {
            CellCoords insertPoint = this.GetSelectedInsertPoint();
            this.InsertColumn(insertPoint.Column);
        }

        /// <summary>
        /// Insert a column in the DataTable at passed index
        /// </summary>
        /// <param name="index">column index at which to insert before</param>
        internal void InsertColumn(int index)
        // Adds a column to the data object at a particular point
        {
            // TODO: Harden the naming mechanism - although may introduce never delete columns
            string colName = this.ColPrefix + this.data.Columns.Count.ToString();
            DataColumn tmpCol = new DataColumn();
            tmpCol.DataType = Type.GetType(ApplicationSettings.Default.DefaultWorksheetDataType);
            tmpCol.ColumnName = colName;
            tmpCol.ExtendedProperties.Add("Units", ApplicationSettings.Default.DefaultWorksheetUnitsColumnText);
            tmpCol.ExtendedProperties.Add("Column Title", ApplicationSettings.Default.DefaultWorksheetColumnTitleText);
            this.data.Columns.Add(tmpCol);
            // If index is -1 then 'insert' the column at the end
            if (index > -1)
            {
                tmpCol.SetOrdinal(index);
                this.RefreshView();
                // Select the newly generated column
                this.ClearSelection();
                this.Columns[index].Selected = true;
            }
            else
            {
                this.RefreshView();
            }
        }

        /// <summary>
        /// Sometimes necessary to rebind data to get current view of DataTable i.e. adding columns
        /// </summary>
        internal void RefreshView()
        {
            this.DataSource = null;
            this.Columns.Clear();
            // Binding data causes columns to be generated with SortMode = Automatic 
            // which is incompatible with ColumnHeaderSelect. Hack to ensure that crash 
            // does not occur when rebinding
            if (this.SelectionMode == DataGridViewSelectionMode.ColumnHeaderSelect)
            {
                this.SelectionMode = DataGridViewSelectionMode.CellSelect;
                this.DataSource = this.dataHandle;
                this.SelectionMode = DataGridViewSelectionMode.ColumnHeaderSelect;
            }
            else
            {
                this.DataSource = this.dataHandle;
            }
        }

        /// <summary>
        /// Calls the Edit Columns dialog for this WorksheetGridView
        /// </summary>
        internal void EditColumns()
        {
            EditColumnsDialog dlg = new EditColumnsDialog(this);
            if (dlg.ShowDialog() == DialogResult.OK)
            {
                for (int i = 0; i < dlg.EditedValues.Count; i++)
                {
                    DataColumn tmpCol = this.data.Columns[dlg.EditedValues[i].DataColumnHeader];
                    tmpCol.ExtendedProperties["Units"] = dlg.EditedValues[i].Units;
                    tmpCol.ExtendedProperties["Column Title"] = dlg.EditedValues[i].ViewableHeader;
                }
                dlg.Dispose();
                // Cache the selection for the refresh
                List<CellCoords> selectionCache = new List<CellCoords>();
                foreach (DataGridViewCell selectedCell in this.SelectedCells)
                {
                    selectionCache.Add(new CellCoords(selectedCell.RowIndex,
                        selectedCell.ColumnIndex));
                }
                this.RefreshView();
                this.ClearSelection();
                foreach (CellCoords cellCoords in selectionCache)
                {
                    this.Rows[cellCoords.Row].Cells[cellCoords.Column].Selected = true;
                }
            }
        }

        /// <summary>
        /// Copies the content of selection to clipboard
        /// </summary>
        internal void Copy()
        {
            try
            {
                Clipboard.SetDataObject(
                    this.GetClipboardContent());
            }
            catch
            {
                Console.WriteLine("Cannot access clipboard");
                // Do nothing?
            }
        }


        private void WorksheetGridView_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
        {
            // TODO: Come up with sensible cell validating behaviour
            DataGridViewCell cell = this.Rows[e.RowIndex].Cells[e.ColumnIndex];
            if (cell.IsInEditMode) {
                Control editingControl = this.EditingControl;
                double result;
                if (System.Double.TryParse(e.FormattedValue.ToString(), out result))
                {
                    editingControl.Text = result.ToString();
                }
                else
                {
                    editingControl.Text = DBNull.Value.ToString();
                }
            }
            
        }


        private void WorksheetGridView_MouseDown(object sender, MouseEventArgs e)
        {
            DataGridView.HitTestInfo hti = this.HitTest(e.X, e.Y);

            // Some hackish code to allow both row and column selections
            if (hti.Type == DataGridViewHitTestType.ColumnHeader)
            {
                if (this.SelectionMode != DataGridViewSelectionMode.ColumnHeaderSelect)
                {
                    this.SelectionMode = DataGridViewSelectionMode.ColumnHeaderSelect;
                }
            }
            else if (hti.Type == DataGridViewHitTestType.RowHeader)
            {
                if (this.SelectionMode != DataGridViewSelectionMode.RowHeaderSelect)
                {
                    this.SelectionMode = DataGridViewSelectionMode.RowHeaderSelect;
                }

            }

            // Sets is so the right-mousedown will select a cell/column/row
            if (hti.Type == DataGridViewHitTestType.Cell)
            {
                
                if (SelectedCells.Count == 1 ||
                    this.Rows[hti.RowIndex].Cells[hti.ColumnIndex].Selected == false)
                {
                    this.ClearSelection();
                    this.Rows[hti.RowIndex].Cells[hti.ColumnIndex].Selected = true;
                }
            }
            else if (hti.Type == DataGridViewHitTestType.RowHeader)
            {
                this.ClearSelection();
                this.Rows[hti.RowIndex].Selected = true;
            }
            else if (hti.Type == DataGridViewHitTestType.ColumnHeader)
            {
                this.ClearSelection();
                this.Columns[hti.ColumnIndex].Selected = true;
            }
            
        }



        private void WorksheetGridView_ColumnAdded(object sender, DataGridViewColumnEventArgs e)
        // Set properties of columns in view when added
        {
            DataColumn dataCol = this.data.Columns[e.Column.DataPropertyName];
            e.Column.HeaderText = dataCol.ColumnName + Environment.NewLine
                + dataCol.ExtendedProperties["Column Title"] + Environment.NewLine
                + "(" + dataCol.ExtendedProperties["Units"] + ")";
            e.Column.SortMode = DataGridViewColumnSortMode.Programmatic;

        }

        protected override void OnRowPostPaint(DataGridViewRowPostPaintEventArgs e)
        // Use OnRowPostPaint event to draw row numbers on row headers
        // Taken from http://www.danielsoper.com/programming/DataGridViewNumberedRows.aspx
        { 
            string strRowNumber = (e.RowIndex + 1).ToString();
            //prepend leading zeros to the string
            while (strRowNumber.Length < this.RowCount.ToString().Length) strRowNumber = "0" + strRowNumber;
            //determine the display size of the row number string using
            //the DataGridView's current font.
            SizeF size = e.Graphics.MeasureString(strRowNumber, this.Font);
            //adjust the width of the column that contains the row header cells 
            //if necessary
            if (this.RowHeadersWidth < (int)(size.Width + 20)) this.RowHeadersWidth = (int)(size.Width + 20);
            //this brush will be used to draw the row number string on the
            //row header cell using the system's current ControlText color
            Brush b = SystemBrushes.ControlText;
            //draw the row number string on the current row header cell using
            //the brush defined above and the DataGridView's default font
            e.Graphics.DrawString(strRowNumber, this.Font, b, e.RowBounds.Location.X + 15, e.RowBounds.Location.Y + ((e.RowBounds.Height - size.Height) / 2));
            //call the base object's OnRowPostPaint method
            base.OnRowPostPaint(e);
        }

        private void WorksheetGridView_CellMouseDoubleClick(object sender, DataGridViewCellMouseEventArgs e)
        {
            // If double click on column header, edit columns
            if (e.RowIndex == -1 && e.Button == MouseButtons.Left)
            {
                this.EditColumns();
            }
        }


    }

}
